package com.yunwushanhe.filter;

import com.alibaba.fastjson.JSON;
import com.yunwushanhe.bean.ret.ResponseResult;
import com.yunwushanhe.constant.Constant;
import com.yunwushanhe.constant.RedisConstant;
import com.yunwushanhe.entity.LoginUser;
import com.yunwushanhe.enums.AppHttpCodeEnum;
import com.yunwushanhe.util.JwtUtil;
import com.yunwushanhe.util.RedisCache;
import com.yunwushanhe.util.WebUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 这个类就是在对自定义添加的token进行认证处理，成功交给下一个过滤器进行SpringSecurity内部的认证处理，认证失败直接在这里就被断掉了
 * （内部认证处理方式需要去搞懂一下，特别是与jwt结合之后，目前看来需要特别注意下认证的环节）
 * 这个过滤器放行了，并不是代表对需要认证的请求已经完全完成认证了，这里只是完成了我们对token的经检验（而token除了可以验证请求外，还可以携带用户信息），
 * 之后还会有 SpringSecurity 内自己定义的过滤器进行认证，一但认证失败会交给认证失败处理器处理（可以重写处理方式，不然就是默认）
 * (这个类验证token的过程是从请求头中获取token中的id，然后在redis中进行查询，有着代表通过，没有就表示失败)
 * (关于请求头中的 token 在这个系统中他是不会自动消失的，即使你以 x 的形势退出浏览器，他也还在，因为没有设置自动销毁时间，但是redis中存储的token信息是会自动消失的，但是以登出接口退出系统，redis中的token信息马上就被销毁掉了)
 * (在前端应该有一个检测，每次进入系统网址，会检测是否有token，若有且token在redis中未过期，则会自动调用登录接口登录，否则是不会自动登录的，需要手动登录以更新token)
 */
@Component // 这个过滤器，不需要认证的请求也会通过，需要认证的请求也要通过，所以下面有一个对token的非空判断。
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(Constant.USER_TOKEN_KEY);
        if(!StringUtils.hasText(token)){ // 这些请求是前端与后端规定好的
            // token为空，说明接口不需要登录，直接放行
            System.out.println(request.getRequestURL()+"==============================================");
            filterChain.doFilter(request,response);// logout请求不带token会被直接放行，不用经过认证
            return;
        }
        // 解析获取userId
        Claims claims = null;
        try {
            claims = JwtUtil.parseJWT(token);
        } catch (Exception exception) {
            exception.printStackTrace();
            // 一旦出现异常，就会终止方法，并向前端返回信息
            ResponseResult<Object> result = ResponseResult.error(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }
        // catch 中如果你没有再 抛出 异常, 那么catch之后的代码是可以继续执⾏的,
        // 但是try中, 报错的那⼀⾏代码之后⼀直到try结束为⽌的这⼀段代码, 是不会再执⾏的。
        String userId = claims.getSubject();
        LoginUser loginUser = redisCache.getCacheObject(RedisConstant.LOGIN_USERINFO_KEY + ":" + userId);

        //如果获取不到（可能是在redis中存储的信息过期了）
        if(ObjectUtils.isEmpty(loginUser)){ // 这里是如何验证token，就是通过判断redis里面还有没有token得信息，没有得话就代表没有登录（可能是token过期或是真的没有登录）
            // 一旦认证失败，就会终止改过滤器，并向前端返回信息，其实这种认证是自己自定义的，不会交给认证失败处理器处理，你交给全局异常处理器处理都可以
            ResponseResult<Object> result = ResponseResult.error(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }

        //存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 把对象信息存入 SecurityContextHolder 类中

        filterChain.doFilter(request, response);
    }
}
